package viper.receiver;

import java.util.NoSuchElementException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;
import java.io.IOException;

import javax.media.Manager;
import javax.media.Player;
import javax.media.Time;
import javax.media.protocol.PushBufferDataSource;
import javax.media.protocol.PushBufferStream;


import viper.call.common.LiveStream;
import viper.diversity.Dediversity;
import viper.main.StateManager;
import viper.main.ViperHeader;

public class CallReceiver implements Runnable {

	public final static CallReceiver INSTANCE = new CallReceiver();
	private CallReceiver(){
		//newByteArr = new byte[divCodeLen][];
	}
	
	private boolean readyToParse = false;

	public synchronized boolean isReadyToParse() {
		return readyToParse;
	}

	public void setReadyToParse(boolean readyToParse) {
		this.readyToParse = readyToParse;
	}

	//Playback--------------------------------------
	private LiveStream rxls;
	private Player player = null;
	private CustomDataSource rxcds;
	//-----------------------------------------------
	//Sliding window parameters---------------------
	TreeMap<Long, byte[][]> windowTree;

	long t_play; //marker for when to play
	private int initial_cushion; //wait this many ms from first packet
	long t_diff; //difference between system time and player, set once.
	
	byte[][] newByteArr;
	int divCodeLen;
	byte[] timeBytes = new byte[8];
	Long thisTimeLong = new Long(0);
	byte[][] timerByteArray;
	Long firstTime = new Long(0);
	//-----------------------------------------------
	//Timer parameters--------------------------------
	private Timer timer = new Timer();
	private int checking_period = 30; //check every 15ms
	public boolean gotFirstPacket = false;
	//-----------------------------------------------
	
	public void run() {

		//determine size of buckets depending on type of diversity code used
		if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_101 || StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_111)
			divCodeLen = 1;
		else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_232)
			divCodeLen = 3;
		else if(StateManager.INSTANCE.getDiversityCode() == ViperHeader.CODE_364)
			divCodeLen = 6;
		
		rxls = new LiveStream();
		
		//Initialize windowTree and timeout depending on div. code
		//byte divCode = StateManager.INSTANCE.getDiversityCode();
		windowTree = new TreeMap<Long, byte[][]>();
		t_play = Long.MAX_VALUE;
		initial_cushion = 600;
		
		try {		
			// Construct a new socket on specified port.
			// All packets received on this port will be assumed to be VIPER packets.
			// Construct the custom PushBufferStream
			// Initialize receive stream with DataSource
			rxcds = new CustomDataSource(rxls);
			rxcds.connect(); 

			// Construct and start the audio player
			player = Manager.createRealizedPlayer(rxcds);
			player.start();
			System.out.println("Starting player...");
		} catch (Throwable t) {
			t.printStackTrace();
		}
		
		readyToParse = true;
		
		System.out.println("run CallReceiver");
	}

	public void parsePacket(byte[] buff) 
	{
		if(!readyToParse) //player is not ready yet
			return;
		
		//parse timestamp
		System.arraycopy(buff, 0, timeBytes, 0, 8);
		//timeBytes = new byte[]{buff[0], buff[1], buff[2], buff[3], buff[4], buff[5], buff[6], buff[7]};
		long timeStamp = ViperHeader.byteArrayToLong(timeBytes);
		if(!gotFirstPacket)
		{
			System.out.println("should not get here more than once");
			t_diff =  System.currentTimeMillis() - timeStamp;
			t_play = timeStamp - initial_cushion;
			gotFirstPacket = true;			
			timer.schedule(new ViperTimerTask(), 0, checking_period);
		}

		//parse pktType
		byte pktType = buff[8];
		//byte pktType = buff[1]; //TODO: make it 0, 1, ...
		//int pktType = 0; //for 1,0,1

		//parse payload
		byte[] data = new byte[buff.length - 9];
		System.arraycopy(buff, 9, data, 0, data.length);

		if((thisTimeLong = timeStamp) > t_play)
		{
			//System.out.println("add: timestamp " + timeStamp + ", t_play " + t_play);
			//retrieve the bucket from hash map at specified pkt type index
			newByteArr = windowTree.get(thisTimeLong);
			
			//if nothing was there, then make a new bucket
			if(newByteArr==null){
				newByteArr = new byte[divCodeLen][];
				/** Debug-Ji
				System.out.println("Null "+divCodeLen);
				//System.out.println("Null");
				**/
			}
			
			///** Debug-Ji
			System.out.println("PKT TYPE: "+(int)pktType);
			//**/
			
			//add necessary pkt data in bucket
			//System.out.println(newByteArr.length);
			newByteArr[(int)pktType] = data;
			
			/** Debug-Ji
			System.out.println("CHECKING STORED BYTE[][]: "+ (newByteArr[0]==null) +" "+(newByteArr[1]==null)+" "+(newByteArr[2]==null));
			
			if(data!=null)
				System.out.println("NOT NULL AT RX");
			**/
			
			//replace bucket into hash map
			windowTree.put(thisTimeLong, newByteArr);			
		}
	}

	private byte[][] dediversify(boolean partial, byte[][] bs) {
		//System.out.println("dediv" + partial);
		
		byte [][] temp = null;
		
		if(divCodeLen==3)
			temp = Dediversity.undiversify(bs[0], bs[1], bs[2], partial);
		else if(divCodeLen==6)
			temp = Dediversity.undiversify(bs[0], bs[1], bs[2], bs[3], bs[4], bs[5], partial);	
		else if(divCodeLen==1)
			temp = bs; //just send to playback as is, there is no diversity code
		else //default case for unknown diversity coding
			System.err.println("Unknown diversity coding detected during dediversification.");
		
		return temp;
	}

	public void end()
	{
		//timer.cancel();
		if(player!=null)
			player.stop();
		if(rxcds!=null)
			try {
				rxcds.stop();
			} catch (IOException e) {
				e.printStackTrace();
			}
		readyToParse = false;
		gotFirstPacket = false;
		System.out.println("stop CallReceiver");
	}

	class CustomDataSource extends PushBufferDataSource {

		protected Object [] controls = new Object[0];
		protected boolean started = false;
		protected String contentType = "raw";
		protected boolean connected = false;
		protected Time duration = DURATION_UNKNOWN;
		protected LiveStream [] streams = null;
		protected LiveStream stream = null;

		public CustomDataSource(LiveStream ls) {
			streams = new LiveStream[1];
			streams[0] = ls;
			stream = ls;
		}

		public String getContentType() {
			if (!connected){
				System.err.println("Error: DataSource not connected");
				return null;
			}
			return contentType;
		}

		public void connect() throws IOException {
			if (connected)
				return;
			connected = true;
		}

		public void disconnect() {
			try {
				if (started)
					stop();
			} catch (IOException e) {}
			connected = false;
		}

		public void start() throws IOException {
			// we need to throw error if connect() has not been called
			if (!connected)
				throw new java.lang.Error("DataSource must be connected before it can be started");
			if (started)
				return;
			started = true;
			stream.start(true);
		}

		public void stop() throws IOException {
			if ((!connected) || (!started))
				return;
			started = false;
			stream.start(false);
		}

		public Object [] getControls() {
			return controls;
		}

		public Object getControl(String controlType) {
			try {
				Class<?>  cls = Class.forName(controlType);
				Object cs[] = getControls();
				for (int i = 0; i < cs.length; i++) {
					if (cls.isInstance(cs[i]))
						return cs[i];
				}
				return null;

			} catch (Exception e) {   // no such controlType or such control
				return null;
			}
		}

		public Time getDuration() {
			return duration;
		}

		public PushBufferStream [] getStreams() {
			if (streams == null) {
				streams = new LiveStream[1];
				stream = streams[0] = new LiveStream();
			}
			return streams;
		}
	}
	
	/**
	 * 
	 * @author Grace
	 * Sources:
	 * http://java.sun.com/products/jfc/tsc/articles/timer/
	 * http://java.sun.com/j2se/1.5.0/docs/api/java/util/TimerTask.html
	 */
	class ViperTimerTask extends TimerTask{
		public void run() {		
			//check current range
			//t_play = System.currentTimeMillis() - t_diff - initial_cushion;
			t_play = t_play + checking_period;
			playAvailable();
		}
		
		public void playAvailable()
		{
			try{
				if((firstTime = windowTree.firstKey()) < t_play)
				{
					if((timerByteArray = windowTree.get(firstTime))!=null)
					{
						//System.out.println("playing: " + firstTime + ", t_play " + t_play);
						byte[][] decoded = dediversify(false, timerByteArray);
						for(int i=0;i<decoded.length;i++)//for(byte[] d : decoded)
						{
							rxls.update(decoded[i]);
							//System.out.println("rx");
						}
						
						windowTree.remove(firstTime); //clean up if played something
						playAvailable(); //recursive
					}
				}
			}
			catch(NoSuchElementException a)
			{
				//no times in windowTree
			}
		}
	}
}